package vacuum.noxray;

import java.util.HashMap;
import java.util.HashSet;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;

import vacuum.noxray.math.Line;
import vacuum.noxray.math.Vector;

public class PlayerVision {
	private static int[] transparent;// = {54, 0, 79, 20, 97, 29, 60, 44, 114, 109, 108, 67, 53, 65, 85, 107, 92, 26, 71, 64, 94, 93, 96, 66, 28, 27, 69, 70, 72, 77, 55, 75, 76, 78, 323, 51, 90, 81, 83, 59, 37, 38, 39, 40, 6, 31, 111, 8, 9, 10, 11};
	private static int[] hidden;// = {54, 52, 56, 48};
	private static int xSearch = 100, ySearch = 100, zSearch = 100;
	private static boolean goThreadSafe;
	private static NoXRay plugin;
	private Player p;
	private boolean verbose;
	private boolean isActuallyOpaque;
	private static HashMap<Player, HashSet<Location>> hiddenBlocks = new HashMap<Player, HashSet<Location>>();
	public PlayerVision(Player p, boolean verbose, boolean isActuallyOpaque){
		this.p = p;
		this.verbose = verbose;
	}

	/*
	 * Efficiency O(n^6), I think
	 */
	public void examineVision(){
		final HashSet<Location> hiddenSet = hiddenBlocks.get(p);
		if(hiddenSet == null){
			if(verbose)
				System.out.println("Error. Player " + p.getName() + " not registered!");
			return;
		}

		Location l = p.getLocation();
		int px = l.getBlockX();
		int py = l.getBlockY();
		int pz = l.getBlockZ();
		for(int i = px - xSearch; i < px + xSearch; i++){
			for (int j = py - ySearch; j < py + ySearch; j++) {
				for (int k = pz - zSearch; k < pz + zSearch; k++) {
					int type = p.getWorld().getBlockTypeIdAt(i, j, k);
					if(containsBinary(hidden, type)){
						final Location loc = new Location(p.getWorld(), i, j, k);
						final Block b = p.getWorld().getBlockAt(loc);
						if(!hasVisionTo(i, j, k)){
							if(!hiddenSet.contains(loc)){
								if(goThreadSafe){
									Bukkit.getScheduler().scheduleAsyncDelayedTask(plugin, new Runnable() {

										@Override
										public void run() {
											if(verbose)
												System.out.println("Hid block " + b.getTypeId() + " at location " + loc.getBlockX() + ", " + loc.getBlockY() + ", " + loc.getBlockZ() + " for player " + p.getName());

											p.sendBlockChange(loc, 0, (byte)0);
											//p.sendBlockChange(loc, 1, (byte)0);
											hiddenSet.add(loc);
										}
									});
								}
								else {
									if(verbose)
										System.out.println("Hid block " + b.getTypeId() + " at location " + i + ", " + j + ", " + k + " for player " + p.getName());

									p.sendBlockChange(loc, 0, (byte)0);
									//p.sendBlockChange(loc, 1, (byte)0);
									hiddenSet.add(loc);
								}
							}
						}
						else{
							if(hiddenSet.contains(loc)){
								if(goThreadSafe){
									Bukkit.getScheduler().scheduleAsyncDelayedTask(plugin, new Runnable() {
										
										@Override
										public void run() {
											if(verbose)
												System.out.println("Revealed block " + b.getTypeId() + " at location " + loc.getBlockX() + ", " + loc.getBlockY() + ", " + loc.getBlockZ() + " for player " + p.getName());
											p.sendBlockChange(loc, 0, (byte)0);
											p.sendBlockChange(loc, b.getType(), b.getData());
											hiddenSet.remove(loc);
										}
									});
								} else {
									if(verbose)
										System.out.println("Revealed block " + b.getTypeId() + " at location " + i + ", " + j + ", " + k + " for player " + p.getName());
									p.sendBlockChange(loc, 0, (byte)0);
									p.sendBlockChange(loc, b.getType(), b.getData());
									hiddenSet.remove(loc);
								}
							}
						}
					}
				}
			}
		}
	}

	private boolean hasVisionTo(int x, int y, int z){
		Location l = p.getLocation();
		int px = l.getBlockX();
		int py = l.getBlockY();
		int pz = l.getBlockZ();
		for (int i = 0; i <= 1; i++) {
			for (int j = 0; j <= 1; j++) {
				for (int k = 0; k <= 1; k++) {
					if(isVisible(getLineBetween(x + i, y + j, z + k, px + (1 - i), py + (1 - j), pz + (1 - k)))){
						return true;
					}
				}
			}
		}
		return false;
	}

	private static Line getLineBetween(int x1, int y1, int z1, int x2, int y2, int z2){
		Vector pos = new Vector(x1, y1, z1);
		Vector dir = new Vector(x2 - x1, y2 - y1, z2 - z1);
		return new Line(pos, dir);
	}

	private boolean isVisible(Line l){
		try{
			for(Vector v : l.getBlocks()){
				Block b = p.getWorld().getBlockAt(v.getX(), v.getY(), v.getZ());
				if(!isTransparent(b.getTypeId()))
					return false;
			}
		} catch (NegativeArraySizeException ex){
			return true;
		} catch (ArithmeticException ex){
			return true;
		}
		return true;
	}

	private boolean isTransparent(int type){
		return containsBinary(transparent, type) ^ isActuallyOpaque;
	}

	public static void setTransparentBlocks(int[] blocks){
		transparent = blocks.clone();

		//bubble sort, efficiency O(n*n). Efficiency is irrelevant, though.
		boolean b = true;
		while(b){
			b = false;
			for (int i = 1; i < transparent.length; i++) {
				if(transparent[i] < transparent[i - 1]){
					int temp = transparent[i];
					transparent[i] = transparent[i - 1];
					transparent[i - 1] = temp;
					b = true;
				}
			}
		}
	}

	public static void setHiddenBlocks(int[] blocks){
		hidden = blocks.clone();

		//bubble sort, efficiency O(n*n). Efficiency is irrelevant.
		boolean b = true;
		while(b){
			b = false;
			for (int i = 1; i < hidden.length; i++) {
				if(hidden[i] < hidden[i - 1]){
					int temp = hidden[i];
					hidden[i] = hidden[i - 1];
					hidden[i - 1] = temp;
					b = true;
				}
			}
		}
	}

	/*
	 * I can make this a binary search instead of a linear search. This will give O(log(n)) instead of O(n)
	 */
	private static boolean containsLinear(int[] ar, int i){
		for (int j = 0; j < ar.length; j++) {
			if(ar[j] == i)
				return true;
		}
		return false;
	}

	private static boolean containsBinary(int[] ar, int i){
		return containsBinary(ar, i, 0, ar.length);
	}

	private static boolean containsBinary(int[] ar, int i, int lo, int hi){
		int pos = (lo + hi) / 2;
		if(ar[pos] == i)
			return true;
		if(lo >= hi || pos == lo || pos == hi)
			return false;
		if(ar[pos] > i)
			return containsBinary(ar, i, lo, pos);
		else
			return containsBinary(ar, i, pos, hi);
	}

	public static void registerPlayer(Player p, boolean verbose, boolean isActuallyOpaque){
		if(hiddenBlocks.get(p) == null){
			hiddenBlocks.put(p, new HashSet<Location>());
			new PlayerVision(p, verbose, isActuallyOpaque).examineVision();
		}
	}

	public static void unregisterPlayer(Player p){
		hiddenBlocks.remove(p);
	}

	public static void dumpPlayerCache(Player p, boolean verbose) {
		for(Location l : hiddenBlocks.get(p)){
			Block b = p.getWorld().getBlockAt(l);
			p.sendBlockChange(l, b.getType(), b.getData());
			if(verbose)
				System.out.println("Revealed block " + b.getTypeId() + " at location " + l.getBlockX() + ", " + l.getBlockY() + ", " + l.getBlockZ() + " for player " + p.getName());
		}
		hiddenBlocks.put(p, new HashSet<Location>());
	}

	public static void setSearchPattern(int x, int y, int z){
		if(x != -1)
			xSearch = x;
		if(y != -1)
			ySearch = y;
		if(z != -1)
			zSearch = z;
	}

	public static void revealAll(boolean verbose){
		for(Player p : hiddenBlocks.keySet()){
			dumpPlayerCache(p, verbose);
		}
		hiddenBlocks.clear();
	}

	public static boolean isInCube(Player p, Location location) {
		return 
				Math.abs(p.getLocation().getBlockX() - p.getLocation().getBlockX()) > xSearch
				|| Math.abs(p.getLocation().getBlockY() - p.getLocation().getBlockY()) > ySearch
				|| Math.abs(p.getLocation().getBlockZ() - p.getLocation().getBlockZ()) > zSearch;
	}

	public static void setPlugin(NoXRay plugin) {
		if(PlayerVision.plugin == null)
			PlayerVision.plugin = plugin;
		else
			throw new UnsupportedOperationException();
	}

	public static void setThreadSafe(boolean threadSafe) {
		goThreadSafe = threadSafe;
	}
}
